﻿#region header

//  ***************************************************************************
// *                                LICENSE
//  ***************************************************************************
//  * "Haplous" .NET Simple 2D Game Framework
//  * 
//  * Copyright © Jonathan Evans 2014-2014
//  * 
//  * This program is free software; you can redistribute it and/or
//  * modify it under the terms of the GNU General Public License
//  * as published by the Free Software Foundation; either version 2
//  * of the License, or (at your option) any later version.
//  * 
//  * This program is distributed in the hope that it will be useful,
//  * but WITHOUT ANY WARRANTY; without even the implied warranty of
//  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  * GNU General Public License for more details.
//  * 
//  * You should have received a copy of the GNU General Public License
//  * along with this program; if not, write to the Free Software
//  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
//  * 
//  ***************************************************************************
//  * 
//  * Contact: opencollar@googlemail.com
//  * 
//  * Website: http://haplous.codeplex.com/
//  *
//  * NuGet:   http://www.nuget.org/packages/haplous/
//  *
//  * File:    Haplous/Haplous/RootDictionary.cs
//  *
//  ***************************************************************************

#endregion

using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Threading;

using Haplous.Correctness;

using JetBrains.Annotations;

namespace Haplous.Core
{
    /// <summary>
    ///     A thread-safe dictionary of root objects keyed on their names. This class cannot be inherited.
    /// </summary>
    /// <typeparam name="T">
    ///     The type of the items the dictionary will contain.  Must be derived from <see cref="ElementBase" />.
    /// </typeparam>
    /// <remarks>The dictionary enforces no duplicate items or keys.</remarks>
    [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")]
    [SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix")]
    [DebuggerDisplay("RootDictionary: Name={Name}")]
    public sealed class RootDictionary<T> : Disposable, IEnumerable<T> where T : class, IElement
    {
        /// <summary>
        ///     The objects to be stored in the dictionary.
        /// </summary>
        /// <remarks>
        ///     Concurrent access is controlled by the <see cref="_lock" /> field.
        /// </remarks>
        [NotNull]
        private readonly Dictionary<string, T> _dictionary = new Dictionary<string, T>();

        /// <summary>
        ///     A lock used to control concurrent access to the <see cref="_dictionary" /> field.
        /// </summary>
        [NotNull]
        private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim();

        /// <summary>
        ///     The name of the property represented.
        /// </summary>
        [NotNull]
        private readonly string _name;

        /// <summary>
        ///     The keys of the elements in the order that they were added.
        /// </summary>
        [NotNull]
        private readonly Collection<string> _orderedKeys = new Collection<string>();

        /// <summary>
        ///     Initializes a new instance of the <see cref="RootDictionary{T}" /> class.
        /// </summary>
        /// <param name="name">The name of the property represented.  The name may not be <see langword="null" /> or whitespace.</param>
        internal RootDictionary([NotNull] string name)
        {
            name.ValidateArgument("name", StringMustNotBe.NullOrEmptyOrWhiteSpace);

            _name = name;
        }

        /// <summary>
        ///     The name of the property represented.
        /// </summary>
        [NotNull]
        public string Name
        {
            get { return _name; }
        }

        /// <summary>
        ///     Gets the item with the specified key.
        /// </summary>
        /// <param name="key">The key identifying the item to return.</param>
        /// <returns>The item requested.</returns>
        [NotNull]
        public T this[[NotNull] string key]
        {
            get
            {
                key.ValidateArgument("key", StringMustNotBe.NullOrEmptyOrWhiteSpace);
                _lock.EnterReadLock();
                try
                {
                    var item = _dictionary[key];
                    if(item == null)
                        throw new KeyNotFoundException(string.Format(CultureInfo.InvariantCulture, "Unable to find any entry with the key: \"{0}\".", key));

                    return item;
                }
                finally
                {
                    _lock.ExitReadLock();
                }
            }
        }

        /// <summary>
        ///     Gets the count of the number of entries in the dictionary.
        /// </summary>
        /// <value>The count of the number of entries in the dictionary.</value>
        public int Count
        {
            get
            {
                _lock.EnterReadLock();
                try
                {
                    return _dictionary.Count;
                }
                finally
                {
                    _lock.ExitReadLock();
                }
            }
        }

        /// <summary>
        ///     The keys of the elements in the order that they were added.
        /// </summary>
        [NotNull]
        public Collection<string> OrderedKeys
        {
            get { return _orderedKeys; }
        }

        #region IEnumerable<T> Members

        /// <summary>
        ///     Returns an enumerator that iterates through the collection.
        /// </summary>
        /// <returns>
        ///     A <see cref="T:System.Collections.Generic.IEnumerator`1" /> that can be used to iterate through the collection.
        /// </returns>
        public IEnumerator<T> GetEnumerator()
        {
            return _dictionary.Values.ToList().GetEnumerator();
        }

        /// <summary>
        ///     Returns an enumerator that iterates through a collection.
        /// </summary>
        /// <returns>
        ///     An <see cref="T:System.Collections.IEnumerator" /> object that can be used to iterate through the collection.
        /// </returns>
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        #endregion

        /// <summary>
        ///     Adds a new item, or replaces an existing entry for the specified item with the new entry supplied.
        /// </summary>
        /// <param name="item">The item to add or replace.</param>
        /// <returns>The previous entry, or <see langword="null" /> if there was no previous entry.</returns>
        /// <exception cref="DuplicateEntryException"></exception>
        [CanBeNull]
        public T Replace([NotNull] T item)
        {
            item.ValidateArgument("item");
            var key = item.Name;
            _lock.EnterWriteLock();
            try
            {
                T value;
                var keyIndex = -1;
                var replace = _dictionary.TryGetValue(key, out value);
                if(replace)
                {
                    keyIndex = _orderedKeys.IndexOf(key);
                    _dictionary.Remove(key);
                    _orderedKeys.Remove(key);
                }
                else
                {
                    if(_dictionary.ContainsValue(item))
                    {
                        var pair = _dictionary.First(e => ReferenceEquals(e.Value, item));

                        throw new DuplicateEntryException(string.Format(CultureInfo.InvariantCulture,
                                                                        "This object (\"{0}\") has already been added to the dictionary, but with a different key: \"{1}\".", key, pair.Key));
                    }
                }

                _dictionary.Add(key, item);
                if(keyIndex >= 0)
                    _orderedKeys.Insert(keyIndex, key);
                else
                    _orderedKeys.Add(key);

                return value;
            }
            finally
            {
                _lock.ExitWriteLock();
            }
        }

        /// <summary>
        ///     Determines whether the dictionary contains an entry with the specified key.
        /// </summary>
        /// <param name="key">The key for which to search.</param>
        /// <returns>
        ///     <see langword="true" /> if the dictionary contains an entry with the specified key; otherwise,
        ///     <see langword="false" />.
        /// </returns>
        public bool Contains([NotNull] string key)
        {
            key.ValidateArgument("key", StringMustNotBe.NullOrEmptyOrWhiteSpace);
            _lock.EnterReadLock();
            try
            {
                return _dictionary.ContainsKey(key);
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }

        /// <summary>
        ///     Determines whether the dictionary contains the item specified.
        /// </summary>
        /// <param name="item">The item for which to search.</param>
        /// <returns><see langword="true" /> if the dictionary contains the item; otherwise, <see langword="false" />.</returns>
        public bool Contains([NotNull] T item)
        {
            item.ValidateArgument("item");
            _lock.EnterReadLock();
            try
            {
                return _dictionary.ContainsValue(item);
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }

        /// <summary>
        ///     Adds the specified item.
        /// </summary>
        /// <param name="item">The item to add.</param>
        /// <exception cref="DuplicateEntryException">
        ///     The item is already in the collection, or another item with the same
        ///     key has already been added.
        /// </exception>
        /// <remarks>The item added will be keyed on its <see cref="ElementBase.Name" /> property.</remarks>
        public void Add([NotNull] T item)
        {
            item.ValidateArgument("item");
            var key = item.Name;

            _lock.EnterUpgradeableReadLock();
            try
            {
                T value;
                if(_dictionary.TryGetValue(key, out value))
                {
                    if(ReferenceEquals(value, item))
                        throw new DuplicateEntryException(string.Format(CultureInfo.InvariantCulture, "This object has already been added to the dictionary using the same key: \"{0}\".", key));

                    throw new DuplicateEntryException(string.Format(CultureInfo.InvariantCulture, "A different object with the same key has already been added to the dictionary: \"{0}\".", key));
                }

                if(_dictionary.ContainsValue(item))
                {
                    var pair = _dictionary.First(e => ReferenceEquals(e.Value, item));

                    throw new DuplicateEntryException(string.Format(CultureInfo.InvariantCulture, "This object (\"{0}\") has already been added to the dictionary, but with a different key: \"{1}\".",
                                                                    key, pair.Key));
                }

                _lock.EnterWriteLock();
                try
                {
                    _dictionary.Add(key, item);
                    _orderedKeys.Add(key);
                }
                finally
                {
                    _lock.ExitWriteLock();
                }
            }
            finally
            {
                _lock.ExitUpgradeableReadLock();
            }
        }

        /// <summary>
        ///     Tries the get value identified by the key given.
        /// </summary>
        /// <param name="key">The key identifying the value to retrieve..</param>
        /// <param name="value">If successful the value is returned in this argument, otherwise <see langword="null" />.</param>
        /// <returns><see langword="true" /> if a corresponding value was found, <see langword="false" /> otherwise.</returns>
        public bool TryGetValue(string key, out T value)
        {
            key.ValidateArgument("key", StringMustNotBe.NullOrEmptyOrWhiteSpace);
            _lock.EnterReadLock();
            try
            {
                return _dictionary.TryGetValue(key, out value);
            }
            finally
            {
                _lock.ExitReadLock();
            }
        }

        /// <summary>
        ///     Removes the item with the specified key.
        /// </summary>
        /// <param name="key">The key identifying the item to remove.</param>
        public void Remove([NotNull] string key)
        {
            _lock.EnterWriteLock();
            try
            {
                _dictionary.Remove(key);
                _orderedKeys.Remove(key);
            }
            finally
            {
                _lock.ExitWriteLock();
            }
        }

        /// <summary>
        ///     Removes the item specified.
        /// </summary>
        /// <param name="item">The item to remove.</param>
        /// <exception cref="EntryNotFoundException">The item could not be found, or a different item was found in its place. </exception>
        public void Remove([NotNull] T item)
        {
            item.ValidateArgument("item");
            var key = item.Name;
            _lock.EnterUpgradeableReadLock();
            try
            {
                T value;
                if(!_dictionary.TryGetValue(key, out value))
                {
                    var pair = _dictionary.FirstOrDefault(e => ReferenceEquals(e.Value, item));

                    if(string.IsNullOrEmpty(pair.Key))
                        throw new EntryNotFoundException(string.Format(CultureInfo.InvariantCulture, "No item with this key could be found: \"{0}\".", key));

                    throw new EntryNotFoundException(string.Format(CultureInfo.InvariantCulture, "The item given was found, but under a different key: \"{0}\".", pair.Key));
                }

                if(!ReferenceEquals(value, item))
                    throw new EntryNotFoundException(string.Format(CultureInfo.InvariantCulture, "The item found (\"{0}\") did not match that given: \"{1}\".", value.Name, key));

                _lock.ExitUpgradeableReadLock();
                try
                {
                    _dictionary.Remove(key);
                    _orderedKeys.Remove(key);
                }
                finally
                {
                    _lock.ExitUpgradeableReadLock();
                }
            }
            finally
            {
                _lock.ExitUpgradeableReadLock();
            }
        }

        /// <summary>
        ///     Tries the remove the item identified by the key given.
        /// </summary>
        /// <param name="key">The key identifying the item to remove.</param>
        /// <returns><see langword="true" /> if the item was found and removed, <see langword="false" /> otherwise.</returns>
        public bool TryRemove([NotNull] string key)
        {
            key.ValidateArgument("key", StringMustNotBe.NullOrEmptyOrWhiteSpace);
            _lock.EnterUpgradeableReadLock();
            try
            {
                if(!_dictionary.ContainsKey(key))
                    return false;

                _lock.ExitUpgradeableReadLock();
                try
                {
                    _dictionary.Remove(key);
                    return true;
                }
                finally
                {
                    _lock.ExitUpgradeableReadLock();
                }
            }
            finally
            {
                _lock.ExitUpgradeableReadLock();
            }
        }

        /// <summary>
        ///     Tries the remove the item given
        /// </summary>
        /// <returns><see langword="true" /> if the item was found and removed, <see langword="false" /> otherwise.</returns>
        public bool TryRemove([NotNull] T item)
        {
            item.ValidateArgument("item");
            var key = item.Name;
            _lock.EnterUpgradeableReadLock();
            try
            {
                T value;
                if(TryGetValue(item.Name, out value))
                {
                    if(!ReferenceEquals(value, item))
                        return false;
                }
                else
                {
                    var pair = _dictionary.FirstOrDefault(e => ReferenceEquals(e.Value, item));

                    if(default(KeyValuePair<string, T>).Equals(pair))
                        return false;
                }

                _lock.ExitUpgradeableReadLock();
                try
                {
                    _dictionary.Remove(key);
                    return true;
                }
                finally
                {
                    _lock.ExitUpgradeableReadLock();
                }
            }
            finally
            {
                _lock.ExitUpgradeableReadLock();
            }
        }

        /// <summary>
        ///     Releases unmanaged and - optionally - managed resources.
        /// </summary>
        /// <param name="disposing">
        ///     <see langword="true" /> to release both managed and unmanaged resources;
        ///     <see langword="false" /> to release only unmanaged resources.
        /// </param>
        protected override void Dispose(bool disposing)
        {
            if(disposing)
            {
                _lock.EnterWriteLock();
                try
                {
                    foreach(var item in _dictionary.Values)
                        item.Dispose();
                    _dictionary.Clear();
                }
                finally
                {
                    _lock.ExitWriteLock();
                }
                _lock.Dispose();
            }

            base.Dispose(disposing);
        }
    }
}